package com.katesoft.scale4j.common.spring;

import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;

import org.springframework.beans.BeanUtils;
import org.springframework.beans.BeansException;
import org.springframework.beans.FatalBeanException;
import org.springframework.beans.MutablePropertyValues;
import org.springframework.beans.PropertyValue;
import org.springframework.beans.SimpleTypeConverter;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.config.ConstructorArgumentValues;
import org.springframework.core.Ordered;
import org.springframework.util.ClassUtils;

import com.katesoft.scale4j.log.LogFactory;
import com.katesoft.scale4j.log.Logger;

/**
 * abstract class for post processing, contains logic for setting bean properties to provided.
 * should be used for internal purpose or exposed with wrapped class.
 * <p/>
 * This class itself does not implement {@link org.springframework.core.Ordered} or
 * {@link org.springframework.core.PriorityOrdered}, but instead define order property and provide
 * getter/setter for this order property. So actual subclasses may just implement Ordered or
 * PriorityOrdered interface and set order.
 * 
 * @author katesoft2007
 */
public abstract class AbstractBeanPropertiesOverridePostProcessor implements
         BeanFactoryPostProcessor {
   protected Logger logger = LogFactory.getLogger(getClass());
   private String targetBean;
   private ConfigurableListableBeanFactory configurableListableBeanFactory;
   private boolean enabled = true;
   private Integer order = Ordered.LOWEST_PRECEDENCE / 2;
   //
   protected Map<Object, Object> beanProperties;
   protected MutablePropertyValues mutablePropertyValues;
   protected ConstructorArgumentValues constructorArgumentValues;

   @Override
   public void postProcessBeanFactory(
            final ConfigurableListableBeanFactory configurableListableBeanFactory)
            throws BeansException {
      this.configurableListableBeanFactory = configurableListableBeanFactory;
      if (enabled) {
         mutablePropertyValues = getBeanDefinition().getPropertyValues();
         constructorArgumentValues = getBeanDefinition().getConstructorArgumentValues();
         doPostProcessing(configurableListableBeanFactory);
      }
   }

   /**
    * overrides bean properties with those, that provided by client.
    */
   protected void overrideBeanProperties() {
      if (beanProperties != null) {
         for (Entry<Object, Object> next : beanProperties.entrySet()) {
            Object beanPropertyValue = next.getValue();
            String beanPropertyName = (String) next.getKey();
            overridePropertyDefinition(beanPropertyName, beanPropertyValue);
         }
      }
   }

   protected void overridePropertyDefinition(String beanPropertyName, Object beanPropertyValue) {
      PropertyValue propertyValue = mutablePropertyValues.getPropertyValue(beanPropertyName);
      Class<?> propertyType = BeanUtils.findPropertyType(beanPropertyName,
               new Class[] { getActualBeanClass() });
      logger.debug("writing %s[%s]=(%s)", beanPropertyName, propertyType.getSimpleName(),
               beanPropertyValue);
      if (propertyValue == null) {
         mutablePropertyValues.addPropertyValue(beanPropertyName, beanPropertyValue);
      } else {
         mutablePropertyValues.removePropertyValue(beanPropertyName);
         propertyValue = new PropertyValue(beanPropertyName, beanPropertyValue);
         mutablePropertyValues.addPropertyValue(propertyValue);
      }
   }

   /**
    * @return actual class of bean definition.
    */
   protected Class<?> getActualBeanClass() {
      try {
         return ClassUtils.forName(configurableListableBeanFactory.getBeanDefinition(targetBean)
                  .getBeanClassName(), ClassUtils.getDefaultClassLoader());
      } catch (final ClassNotFoundException e) {
         throw new FatalBeanException("Unable to load class meta-information", e);
      }
   }

   /**
    * attempt to covert given object to map
    * 
    * @param value
    *           map/properties/string
    * @return properties
    */
   protected Properties convertToProperties(Object value) {
      if (value != null) {
         SimpleTypeConverter converter = new SimpleTypeConverter();
         return converter.convertIfNecessary(value, Properties.class);
      } else {
         return new Properties();
      }
   }

   /**
    * @return bean definition of targetBean.
    */
   protected BeanDefinition getBeanDefinition() {
      if (configurableListableBeanFactory.containsBeanDefinition(targetBean)) {
         return configurableListableBeanFactory.getBeanDefinition(targetBean);
      }
      throw new NoSuchBeanDefinitionException(
               String.format(
                        "No bean named [%s] exists within the bean factory. Cannot post process bean definitions!",
                        targetBean));
   }

   /**
    * client bean properties override
    * 
    * @param beanProperties
    *           properties to override(apply)
    */
   public void setBeanProperties(Map<Object, Object> beanProperties) {
      this.beanProperties = beanProperties;
   }

   /**
    * allows to disable this post processor.
    * 
    * @param enabled
    *           true or false flag
    */
   public void setEnabled(boolean enabled) {
      this.enabled = enabled;
   }

   public int getOrder() {
      return order == null ? 0 : order;
   }

   /**
    * The obvious order the post processors execute would of course be the order in which they’re
    * defined in the XML file, one of the others said.
    * <p/>
    * Unfortunately, you can’t rely on the order of the beans defined in a Spring XML file.
    * <p/>
    * The order in which they’re defined is not guaranteed to be the order in which they’re
    * executed.
    * <p/>
    * This method allows to set the order of bean factory post processor.
    * 
    * @param order
    *           preferred execution order of this post processor.
    */
   public void setOrder(Integer order) {
      this.order = order;
   }

   /**
    * set the bean name. This property is not required in this abstract class.
    * 
    * @param targetBean
    *           name/id of spring bean.
    */
   protected void setTargetBean(String targetBean) {
      this.targetBean = targetBean;
   }

   /**
    * @return name of the target bean for properties overriding.
    */
   public String getTargetBean() {
      return targetBean;
   }

   /**
    * perform actual post-processing.
    * 
    * @param configurableListableBeanFactory
    *           bean factory
    */
   protected abstract void doPostProcessing(
            ConfigurableListableBeanFactory configurableListableBeanFactory);
}
